/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * Dr. Mind - a computer clone of the MasterMind (TM) board game           *
 * Copyright (C) 2019 Mateusz Viste                                        *
 *                                                                         *
 * http://drmind.sourceforge.net                                           *
 *                                                                         *
 * Published under the CC BY-ND 4.0 license (see license.txt).             *
 *                                                                         *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

 NOTES:

 All pictures are stored in an 8-bit and compressed using a custom RLE
 format, similar to what is used by PCX and TGA formats but using a per-file
 RLE repeat bit width (first byte of each image contains its RLE bitmask).

 Video mode is 13h, ie. 320x200 with an 8-bit palette (VGA / MCGA).

 The palette is split in two: first 240 indexes are reserved for pictures and
 are changed every time a new background image is loaded. The last 16 indexes
 are reserved for UI elements (sprites) and are loaded only once, at game
 startup. It is worth noting that the palette's first color (index 0) has a
 special meaning: it is used by the VGA hardware for coloring the overscan.

 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include <dos.h>    /* int86() */
#include <stdio.h>  /* FILE */
#include <stdlib.h> /* malloc() */
#include <string.h> /* memset(), memcpy(), memcmp() */

#define PVER "1.0"
#define PDATE "2019"

/* VGA framebuffer */
static unsigned char far *screenptr = (unsigned char far*)0xA0000000l;

#define BLURVALUE 24
#define BGSIZE 64000u
#define UIWIDTH 144u
#define UISIZE (UIWIDTH * 75u)

#define KEYCODE_UP    0x148
#define KEYCODE_DOWN  0x150
#define KEYCODE_LEFT  0x14B
#define KEYCODE_RIGHT 0x14D
#define KEYCODE_ESC   0x1B
#define KEYCODE_ENTER 0xD

/* OpenWatcom needs conio.h for inp() and outp() */
#ifdef __WATCOMC__
  #include <conio.h>
  #define inportb inp
#endif


/* flushes keyboard, then waits for a keypress and return it. extended keys
 * are ORed with 0x100. */
static int keyb_getkey(void) {
  union REGS regs;
  int res;
  /* flush keyboard and read key data */
  regs.x.ax = 0x0C08;  /* ah=0Ch (flush buff) al=08h (read key) */
  int86(0x21, &regs, &regs);
  res = regs.h.al;
  if (res == 0) { /* extended key - poll again (but without flushing now) */
    regs.h.ah = 0x08;
    int86(0x21, &regs, &regs);
    res = regs.h.al | 0x100;
  }
  return(res);
}

/* init (or close) mode 13h, returns non-zero on error */
static int videoinit(int closeflag) {
  static short prevmode;
  short mode = 0x13;
  union REGS regs;
  /* if initing, then save current mode to restore it later */
  if (closeflag == 0) {
    regs.h.ah = 0x0F;
    int86(0x10, &regs, &regs);
    prevmode = regs.h.al;
  } else {
    mode = prevmode;
  }
  /* now switch video mode */
  regs.x.ax = mode; /* ie. AH=0 AL=mode */
  int86(0x10, &regs, &regs);
  /* has it worked? check current video mode */
  regs.x.ax = 0x0F00; /* make sure to reset AL to avoid false positive due to AL retention */
  int86(0x10, &regs, &regs);
  if (regs.h.al == mode) return(0);  /* good */
  return(-1); /* mode change didn't work */
}

/* Wait for vblank (works both in mode 0x04 and mode 0x13) */
static void waitvblank(void) {
  unsigned char Status;
  do {
    Status = inportb(0x3DA);
  } while((Status & 0x08));
  do {
    Status = inportb(0x3DA);
  } while(!(Status & 0x08));
}

/* load count colors of palette from array of rgb triplets */
static void setpal(const unsigned char *pal, int count, int offset) {
  int i;
  waitvblank();
  for (i = 0; i < count; i++) {
    outp(0x3C8, i + offset); /* color index */
    outp(0x3C9, pal[i + i + i]);     /* R */
    outp(0x3C9, pal[i + i + i + 1]); /* G */
    outp(0x3C9, pal[i + i + i + 2]); /* B */
  }
}

static void loaddata(unsigned char *ptr, FILE *fd, unsigned short len) {
  int b;
  while (len != 0) {
    b = getc(fd);
    if (b == EOF) return;
    *ptr = b;
    ptr++;
    len--;
  }
}

/* unpacks RLE data from stream fd, returns 0 on success */
static int loadrledata(unsigned char *ptr, FILE *fd, unsigned short explen) {
  unsigned int rle, rlebitmask;
  int bytebuff;
  /* read and validate the RLEBITLEN parameter */
  rlebitmask = getc(fd);
  /* decompress the stream */
  while (explen != 0) {
    bytebuff = getc(fd);
    if (bytebuff == EOF) return(-3); /* EOF? */
    if ((bytebuff & rlebitmask) != rlebitmask) { /* if it's a raw byte, save it once */
      *ptr = bytebuff;
      ptr++;
      explen--;
      continue;
    }
    /* if it's a RLE marker, read the next byte */
    rle = (bytebuff ^ rlebitmask) + 1; /* reset RLE marker bits and increment */
    bytebuff = getc(fd);
    if (rle > explen) return(-4);
    explen -= rle;
    for (; rle > 0; rle--) {
      *ptr = bytebuff;
      ptr++;
    }
  }
  return(0);
}

static void loadbg(unsigned char *bg, const char *fname) {
  FILE *fd;
  fd = fopen(fname, "rb");
  if (fd == NULL) return;

  /* read and apply palette */
  loaddata(bg, fd, 240*3);
  setpal(bg, 240, 0);

  /* read pixel data */
  loadrledata(bg, fd, BGSIZE);

  fclose(fd);
}

/* draws a 320x200 background over the screen */
static void draw_bg(const unsigned char *bg, int blurflag) {
  if (blurflag == 0) {
    memcpy(screenptr, bg, BGSIZE);
  } else {
    unsigned short i;
    for (i = 0; i < (BGSIZE + BLURVALUE); i += BLURVALUE) {
      memset(screenptr + i, bg[i], BLURVALUE);
    }
  }
}

static void unblur(const unsigned char *bg) {
  int i, y;
  const char blur[8] = {1, 5, 3, 7, 2, 6, 4, 0};
  draw_bg(bg, 1); /* remove all UI elements from screen */
  for (i = 0; i < 8; i++) {
    waitvblank();
    waitvblank();
    waitvblank();
    waitvblank();
    for (y = blur[i]; y < 200; y += 8) {
      memcpy(screenptr + (y * 320), bg + (y * 320), 320); /* faster than blit_sprite() */
    }
  }
}

/* blits sprite [x,y] of w x h to screen at [dx, dy], color 255 is transparent */
static void blit_sprite(const unsigned char *spr, int sprlinelen, int x, int y, int w, int h, int dx, int dy) {
  int i;
  unsigned char far *dst;
  dst = screenptr + (dy * 320) + dx;
  spr += (y * sprlinelen) + x;
  while (h-- > 0) {
    for (i = 0; i < w; i++) {
      if (spr[i] == 0xff) continue;
      dst[i] = spr[i];
    }
    spr += sprlinelen;
    dst += 320;
  }
}

static void compute_points(unsigned char *flags, const unsigned char *board, const unsigned char *solution) {
  unsigned char s[4], b[4];
  int flagcount = 0;
  int i, ii;
  memset(flags, 0, 4);
  memcpy(s, solution, 4); /* make a copy so I can modify it */
  memcpy(b, board, 4);    /* make a copy so I can modify it */
  /* look for correct places first */
  for (i = 0; i < 4; i++) {
    if (b[i] == s[i]) {
      flags[flagcount++] = 2; /* red */
      s[i] = 0xff; /* do not count it any more */
      b[i] = 0xfe;
    }
  }
  /* look for any color match */
  for (i = 0; i < 4; i++) {
    for (ii = 0; ii < 4; ii++) {
      if (b[i] == s[ii]) {
        flags[flagcount++] = 1; /* white */
        s[ii] = 0xff; /* do not count it any more */
        break;
      }
    }
  }
}

static void draw_balls(const unsigned char *ui, const unsigned char *board, int x, int y) {
  int i;
  unsigned short XPOS[4] = {100, 124, 148, 172};
  const unsigned char YPOS[9] = {166, 145, 124, 103, 82, 61, 40, 19, 30}; /* last one is the solution row */
  const unsigned char SPR_BALLX[9] = { 0, 14, 28, 42, 56, 70,  84,  98, 112};
  if (y > 7) { /* solution print */
    y = 8;
    XPOS[0] = 241;
    XPOS[1] = 260;
    XPOS[2] = 279;
    XPOS[3] = 298;
  }
  for (i = 0; i < 4; i++) {
    blit_sprite(ui, UIWIDTH, SPR_BALLX[board[i]], 33, 14, 13, XPOS[i]+1, YPOS[y]+1);
    /* draw (or hide) the selector */
    if (x == i) {
      blit_sprite(ui, UIWIDTH, 0, 46, 16, 15, XPOS[i], YPOS[y]);
    } else if (y < 8) { /* erase selector, unless we're dealing with a solution print */
      blit_sprite(ui, UIWIDTH, 16, 46, 16, 15, XPOS[i], YPOS[y]);
    }
  }
}

/* this is actually not a "random" function at all - it returns the lowest
 * 16 bits of the 18.2 hz tick timer... but for my need it is good enough */
static unsigned short rnd16(void) {
  union REGS r;
  r.h.ah = 0;
  int86(0x1a, &r, &r); /* get system timer (result in CX:DX) */
  return(r.x.dx);
}

/* works only for integers in the range 0..99 */
/* I use such contraption to avoid linking in printf() (saves 1.5K with TC 2.01) */
static void num2datfname(char *s, unsigned short i) {
  memcpy(s, "000.dat", 8);
  s[1] += i / 10;
  s[2] += i % 10;
}

/* blacks out the screen (and prints "loading...") */
static void loadscreen(const unsigned char *ui) {
  setpal((unsigned char *)"\0\0\0", 1, 0); /* force color #0 to black (avoids ugly overscan) */
  memset(screenptr, 240, BGSIZE); /* stylish dark background (but not black) */
  blit_sprite(ui, UIWIDTH, 0, 61, 31, 4, (320-32)/2, 93);
}

static int play(const unsigned char *ui, unsigned char *bg, unsigned short bgcount, unsigned char easymode) {
  static unsigned short lastimg = 0xff; /* remember last image so I try not repeating it */
  int x, y, i;
  unsigned char board[4], solution[4], hints[4];
  const unsigned char PTSPOSX[2] = {208, 216};
  const unsigned char PTSPOSY[8] = {168, 147, 126, 105, 84, 63, 42, 21};
  unsigned short r, maxcolor = 8;

  /* choose and load a random background picture (unless running in LITE mode) */
  if (bgcount > 0) {
    char bgfname[8];
    RESELECTIMG:
    r = rnd16() % bgcount;
    if (r == lastimg) { /* do not repeat last pic */
      for (i = 0; i < 5; i++) { /* V-Blank periods are about 14ms (70hz)   */
        waitvblank();           /* I wait a little bit because my "random" */
      }                         /* routine is derived from the PIT counter */
      lastimg = 0xff; /* I only reselect the picture ONE TIME to avoid an inf.
                         loop: the rnd routine might return the same value all
                         the time on some platforms, or I might have only one
                         image available... better safe than sorry! */
      goto RESELECTIMG;
    }
    lastimg = r; /* remember pic for next time */
    num2datfname(bgfname, r);
    loadbg(bg, bgfname);
    draw_bg(bg, 1);
  }

  /* */
  if (easymode != 0) maxcolor = 6;

  memset(board, 0, sizeof(board)); /* reset board */
  x = 0;
  y = 0;
  /* compute a "random" combination */
  r = rnd16();
  for (i = 0; i < 4; i++) {
    solution[i] = (r % maxcolor) + 1;
    r >>= 3;
  }

  /* draw upper bar */
  blit_sprite(ui, UIWIDTH, 0, 0, 144, 6, (320 - 144) / 2, 10);
  /* draw 8x empty rows */
  for (i = 16; i < 170; i += 21) {
    blit_sprite(ui, UIWIDTH, 0, 6, 144, 21, (320 - 144) / 2, i);
  }
  /* draw lower bar */
  blit_sprite(ui, UIWIDTH, 0, 27, 144, 6, (320 - 144) / 2, 184);

  for (;;) {
    /* draw my balls */
    draw_balls(ui, board, x, y);
    /* draw_balls(ui, solution, 9, 7); */  /* debug only */
    /* wait for some action */
    switch (keyb_getkey()) {
      case KEYCODE_ESC:
        return(1);
      case KEYCODE_UP:
        board[x]++;
        if (board[x] > maxcolor) board[x] = 0;
        break;
      case KEYCODE_DOWN:
        board[x]--;
        if (board[x] > maxcolor) board[x] = maxcolor; /* unsigned char wrap */
        break;
      case KEYCODE_LEFT:
        if (x > 0) x--;
        break;
      case KEYCODE_RIGHT:
        if (x < 3) x++;
        break;
      /* case 'w': */ /* cheat mode for debugging */
      /*  memcpy(board, solution, 4); */
      case KEYCODE_ENTER:
        /* ignore if all pegs are empty */
        if ((board[0] | board[1] | board[2] | board[3]) == 0) break;
        /* compute points and draw them on screen */
        compute_points(hints, board, solution);
        for (i = 0; i < 4; i++) {
          int secrow = 0;
          const unsigned char sprhints[3] = {0, 133, 126};
          if (i > 1) secrow = 6; /* adjust y-displacement for 2nd row of points */
          if (hints[i] != 0) {
            blit_sprite(ui, UIWIDTH, sprhints[hints[i]], 33, 7, 5, PTSPOSX[i & 1], PTSPOSY[y] + secrow);
          }
        }
        /* redraw my balls to make the selector disappear */
        draw_balls(ui, board, 0xff, y);
        /* see what to do next */
        if (memcmp(board, solution, 4) == 0) {
          blit_sprite(ui, UIWIDTH,  31, 60, 12, 15, 120, 91);  /* G */
          blit_sprite(ui, UIWIDTH,  94, 60, 12, 15, 140, 91);  /* O */
          blit_sprite(ui, UIWIDTH,  94, 60, 12, 15, 160, 91);  /* O */
          blit_sprite(ui, UIWIDTH, 126, 38, 11, 15, 180, 91);  /* D */
          blit_sprite(ui, UIWIDTH, 137, 38,  5, 15, 198, 91);  /* ! */
          if (bgcount > 0) {
            for (i = 0; i < 63; i++) waitvblank(); /* wait a short bit (about 900ms) */
            unblur(bg);
          }
          if (keyb_getkey() == 0x1b) return(1);
          return(0);
        } else if (y == 7) { /* game over */
          blit_sprite(ui, UIWIDTH, 31, 58, 113, 17, (320-113)/2, 91); /* GAME OVER */
          /* solution box */
          blit_sprite(ui, UIWIDTH, 0, 0, 69, 6, 237, 10); /* upper left bar */
          blit_sprite(ui, UIWIDTH, 132, 0, 12, 6, 306, 10); /* upper right bar */
          blit_sprite(ui, UIWIDTH, 0, 27, 69, 6, 237, 42); /* lower left bar */
          blit_sprite(ui, UIWIDTH, 132, 27, 12, 6, 306, 42); /* lower right bar */
          for (i = 16; i < 42; i++) {
            blit_sprite(ui, UIWIDTH, 0, 3, 69, 1, 237, i); /* right body */
            blit_sprite(ui, UIWIDTH, 132, 3, 12, 1, 306, i); /* left body */
          }
          /* "solution" string */
          blit_sprite(ui, UIWIDTH, 32, 46, 75, 12, 240, 15);
          /* actual solution (balls) */
          draw_balls(ui, solution, 0xff, 0xff);
          /* waitkey */
          keyb_getkey();
          return(1);
        } else { /* next try */
          y++;
          memset(board, 0, sizeof(board));
        }
        break;
    }
  }
}

/* returns 0 if fname exists */
static int fileexists(char *fname) {
  FILE *fd;
  fd = fopen(fname, "rb");
  if (fd == NULL) return(-1);
  fclose(fd);
  return(0);
}

/* counts available pictures and returns result */
static unsigned short countpics(void) {
  unsigned short i;
  char fname[8];
  for (i = 0; i < 100; i++) {
    num2datfname(fname, i);
    if (fileexists(fname) != 0) break;
  }
  return(i);
}

static int frontui(const unsigned char *ui, unsigned char *bg, unsigned char *easymode) {
  int pos = 0, i;
  const unsigned char SELECTPUSSY[3] = {64, 80, 96};
  loadbg(bg, "front.dat");
  draw_bg(bg, 0);
  /* draw key pegs in help section */
  blit_sprite(ui, UIWIDTH, 126, 33, 7, 5, 12, 180); /* red */
  blit_sprite(ui, UIWIDTH, 133, 33, 7, 5, 12, 192); /* white */

  /* hide the '6' */
  blit_sprite(bg, 320, 200, 64, 7, 8, 196, 83);

  /* draw selector at initial position */
  blit_sprite(ui, UIWIDTH, 107, 46, 14, 12, 120, SELECTPUSSY[pos]); /* show selector */

  /* copy selector to bg at [2,2] with a 2 px top/bottom margin so I can use
   * it with proper background during animations - this implies that I will
   * not use bg from now on, not to blit it all on screen at least */
  for (i = -2; i < 14; i++) {
    memcpy(bg + (320 * (2 + i)), screenptr + ((SELECTPUSSY[pos] + i) * 320) + 120, 14);
  }

  /* */
  for (;;) {
    /* display color setup count */
    blit_sprite(bg, 320, 188 + (*easymode * 8), 83, 7, 8, 188, 83);
    /* wait for key */
    switch (keyb_getkey()) {
      case KEYCODE_UP:
        if (pos == 0) break;
        pos--;
        for (i = SELECTPUSSY[pos + 1]; i >= SELECTPUSSY[pos]; i -= 2) {
          waitvblank();
          blit_sprite(bg, 320, 0, 1, 14, 15, 120, i);
        }
        break;
      case KEYCODE_DOWN:
        if (pos == 2) break;
        pos++;
        for (i = SELECTPUSSY[pos - 1]; i <= SELECTPUSSY[pos]; i += 2) {
          waitvblank();
          blit_sprite(bg, 320, 0, 0, 14, 15, 120, i);
        }
        break;
      case KEYCODE_ESC:
        pos = 2;   /* fall-through */
      case KEYCODE_ENTER:
        if (pos != 1) return(pos);
        *easymode ^= 1;
        break;
    }
  }
}

static int loadui(unsigned char *ui, const char *fname) {
  FILE *fd;
  int i;
  fd = fopen(fname, "rb");
  if (fd == NULL) return(-1);

  /* read and apply palette */
  loaddata(ui, fd, 16*3);
  setpal(ui, 16, 240);

  /* read pixel data and move its colors to end of palette */
  loadrledata(ui, fd, UISIZE);
  fclose(fd);
  for (i = 0; i < UISIZE; i++) ui[i] += 240;
  return(0);
}

/* prints a DOS-style string ($-terminated) on screen */
static void dosprint(char *s) {
  union REGS r;
  struct SREGS sr;
  r.h.ah = 9; /* DOS 1+ Write string to std out */
  sr.ds = FP_SEG(s);
  r.x.dx = FP_OFF(s);
  int86x(0x21, &r, &r, &sr);
}


int main(void) {
  unsigned char *bg, *ui;
  int i;
  unsigned char easymode = 0;
  unsigned short bgcount;

  if (videoinit(0) != 0) {
    dosprint("ERR: video init failure (requires VGA or MCGA)\r\n$");
    return(1);
  }

  bg = malloc(BGSIZE);
  ui = malloc(UISIZE);
  if ((bg == NULL) || (ui == NULL)) {
    free(bg);
    free(ui);
    videoinit(1);
    dosprint("ERR: out of memory\r\n$");
    return(1);
  }

  if (loadui(ui, "ui.dat") != 0) {
    free(bg);
    free(ui);
    videoinit(1);
    dosprint("ERR: failed to load UI.DAT\r\n$");
    return(1);
  }

  loadscreen(ui);

  if (fileexists("front.dat") != 0) {
    free(bg);
    free(ui);
    videoinit(1);
    dosprint("ERR: failed to load FRONT.DAT\r\n$");
    return(1);
  }

  bgcount = countpics();

  for (;;) {
    i = frontui(ui, bg, &easymode);
    if (i == 0) {  /* play */
      loadscreen(ui);
      do {
        i = play(ui, bg, bgcount, easymode);
        loadscreen(ui);
      } while (i == 0);
    } else { /* quit */
      break;
    }
  }

  free(bg);
  free(ui);

  videoinit(1);

  dosprint("Dr. Mind v" PVER " Copyright (C) " PDATE " Mateusz Viste\r\n"
           "Code and art released under CC BY-ND 4.0 (see license.txt)\r\n$");

  if (bgcount == 0) {
    dosprint("\r\n"
             "Note: You just played the LITE version of Dr. Mind (without pictures).\r\n"
             "      The full version is available at http://drmind.sourceforge.net\r\n$");
  }

  return(0);
}
